{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "b518b04cbfe0"
      },
      "source": [
        "##### Copyright 2020 The TensorFlow Authors."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "cellView": "form",
        "id": "906e07f6e562"
      },
      "outputs": [],
      "source": [
        "#@title Licensed under the Apache License, Version 2.0 (the \"License\");\n",
        "# you may not use this file except in compliance with the License.\n",
        "# You may obtain a copy of the License at\n",
        "#\n",
        "# https://www.apache.org/licenses/LICENSE-2.0\n",
        "#\n",
        "# Unless required by applicable law or agreed to in writing, software\n",
        "# distributed under the License is distributed on an \"AS IS\" BASIS,\n",
        "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
        "# See the License for the specific language governing permissions and\n",
        "# limitations under the License."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "e2d97c7e31aa"
      },
      "source": [
        "# 하위 클래스화를 통한 새로운 레이어 및 모델 만들기"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "4e352274064f"
      },
      "source": [
        "<table class=\"tfo-notebook-buttons\" align=\"left\">\n",
        "  <td><a target=\"_blank\" href=\"https://www.tensorflow.org/guide/keras/custom_layers_and_models\"><img src=\"https://www.tensorflow.org/images/tf_logo_32px.png\">TensorFlow.org에서 보기</a></td>\n",
        "  <td><a target=\"_blank\" href=\"https://colab.research.google.com/github/tensorflow/docs-l10n/blob/master/site/ko/guide/keras/custom_layers_and_models.ipynb\"><img src=\"https://www.tensorflow.org/images/colab_logo_32px.png\">Google Colab에서 실행하기</a></td>\n",
        "  <td><a target=\"_blank\" href=\"https://github.com/tensorflow/docs-l10n/blob/master/site/ko/guide/keras/custom_layers_and_models.ipynb\"><img src=\"https://www.tensorflow.org/images/GitHub-Mark-32px.png\">GitHub에서소스 보기</a></td>\n",
        "  <td><a href=\"https://storage.googleapis.com/tensorflow_docs/docs-l10n/site/ko/guide/keras/custom_layers_and_models.ipynb\"><img src=\"https://www.tensorflow.org/images/download_logo_32px.png\">노트북 다운로드하기</a></td>\n",
        "</table>"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "8d4ac441b1fc"
      },
      "source": [
        "## 설정"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "4e7dce39dd1d"
      },
      "outputs": [],
      "source": [
        "import tensorflow as tf\n",
        "from tensorflow import keras"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "7b363673d96c"
      },
      "source": [
        "## `Layer` 클래스: 상태(가중치)와 일부 계산의 조합\n",
        "\n",
        "Keras의 주요 추상화 중 하나는 `Layer` 클래스입니다. 레이어는 상태(레이어의 \"가중치\")와 입력에서 출력으로의 변환(\"호출, 레이어의 정방향 패스\")을 모두 캡슐화합니다.\n",
        "\n",
        "다음은 밀집 레이어입니다. 상태는 변수 `w` 및 `b`입니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "59b8317dbd3c"
      },
      "outputs": [],
      "source": [
        "class Linear(keras.layers.Layer):\n",
        "    def __init__(self, units=32, input_dim=32):\n",
        "        super(Linear, self).__init__()\n",
        "        w_init = tf.random_normal_initializer()\n",
        "        self.w = tf.Variable(\n",
        "            initial_value=w_init(shape=(input_dim, units), dtype=\"float32\"),\n",
        "            trainable=True,\n",
        "        )\n",
        "        b_init = tf.zeros_initializer()\n",
        "        self.b = tf.Variable(\n",
        "            initial_value=b_init(shape=(units,), dtype=\"float32\"), trainable=True\n",
        "        )\n",
        "\n",
        "    def call(self, inputs):\n",
        "        return tf.matmul(inputs, self.w) + self.b\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "dac8fb03a642"
      },
      "source": [
        "파이썬 함수와 매우 유사한 일부 텐서 입력에서 레이어를 호출하여 레이어를 사용합니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "cdcd15d5e68a"
      },
      "outputs": [],
      "source": [
        "x = tf.ones((2, 2))\n",
        "linear_layer = Linear(4, 2)\n",
        "y = linear_layer(x)\n",
        "print(y)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "382960020a56"
      },
      "source": [
        "가중치 `w`와 `b`는 레이어 속성으로 설정될 때 레이어에 의해 자동으로 추적됩니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "d3d875af9465"
      },
      "outputs": [],
      "source": [
        "assert linear_layer.weights == [linear_layer.w, linear_layer.b]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "ec9d72aa7538"
      },
      "source": [
        "레이어에 가중치를 추가하는 더 빠른 바로 가기에 액세스할 수도 있습니다. `add_weight()` 메서드:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "168548eba841"
      },
      "outputs": [],
      "source": [
        "class Linear(keras.layers.Layer):\n",
        "    def __init__(self, units=32, input_dim=32):\n",
        "        super(Linear, self).__init__()\n",
        "        self.w = self.add_weight(\n",
        "            shape=(input_dim, units), initializer=\"random_normal\", trainable=True\n",
        "        )\n",
        "        self.b = self.add_weight(shape=(units,), initializer=\"zeros\", trainable=True)\n",
        "\n",
        "    def call(self, inputs):\n",
        "        return tf.matmul(inputs, self.w) + self.b\n",
        "\n",
        "\n",
        "x = tf.ones((2, 2))\n",
        "linear_layer = Linear(4, 2)\n",
        "y = linear_layer(x)\n",
        "print(y)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "070ea9b4db6c"
      },
      "source": [
        "## 레이어는 훈련 불가능한 가중치를 가질 수 있습니다\n",
        "\n",
        "훈련 가능한 가중치 외에도 훈련 불가능한 가중치를 레이어에 추가할 수 있습니다. 이러한 가중치는 레이어를 훈련할 때 역전파 동안 고려되지 않아야 합니다.\n",
        "\n",
        "훈련 불가능한 가중치를 추가 및 사용하는 방법은 다음과 같습니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "7c4cb404145f"
      },
      "outputs": [],
      "source": [
        "class ComputeSum(keras.layers.Layer):\n",
        "    def __init__(self, input_dim):\n",
        "        super(ComputeSum, self).__init__()\n",
        "        self.total = tf.Variable(initial_value=tf.zeros((input_dim,)), trainable=False)\n",
        "\n",
        "    def call(self, inputs):\n",
        "        self.total.assign_add(tf.reduce_sum(inputs, axis=0))\n",
        "        return self.total\n",
        "\n",
        "\n",
        "x = tf.ones((2, 2))\n",
        "my_sum = ComputeSum(2)\n",
        "y = my_sum(x)\n",
        "print(y.numpy())\n",
        "y = my_sum(x)\n",
        "print(y.numpy())"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "40f5b74d3d87"
      },
      "source": [
        "`layer.weights`의 일부이지만, 훈련 불가능한 가중치로 분류됩니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "3d4db4ef4fa4"
      },
      "outputs": [],
      "source": [
        "print(\"weights:\", len(my_sum.weights))\n",
        "print(\"non-trainable weights:\", len(my_sum.non_trainable_weights))\n",
        "\n",
        "# It's not included in the trainable weights:\n",
        "print(\"trainable_weights:\", my_sum.trainable_weights)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "fe6942aff7c6"
      },
      "source": [
        "## 모범 사례: 입력 형상이 알려질 때까지 가중치 생성 지연하기\n",
        "\n",
        "위의 `Linear` 레이어는 `__init__()`에서 가중치 `w` 및 `b`의 형상을 계산하는 데 사용되는 `input_dim` 인수를 사용했습니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "275b68d5ea9f"
      },
      "outputs": [],
      "source": [
        "class Linear(keras.layers.Layer):\n",
        "    def __init__(self, units=32, input_dim=32):\n",
        "        super(Linear, self).__init__()\n",
        "        self.w = self.add_weight(\n",
        "            shape=(input_dim, units), initializer=\"random_normal\", trainable=True\n",
        "        )\n",
        "        self.b = self.add_weight(shape=(units,), initializer=\"zeros\", trainable=True)\n",
        "\n",
        "    def call(self, inputs):\n",
        "        return tf.matmul(inputs, self.w) + self.b\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "5ebcacebb348"
      },
      "source": [
        "대부분의 경우, 입력의 크기를 미리 알지 못할 수 있으며, 레이어를 인스턴스화한 후 얼마 지나지 않아 해당 값을 알게 되면 가중치를 지연 생성하고자 합니다.\n",
        "\n",
        "Keras API에서는 레이어의 `build(self, inputs_shape)` 메서드에서 레이어 가중치를 만드는 것이 좋습니다. 다음과 같습니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "118c899f427e"
      },
      "outputs": [],
      "source": [
        "class Linear(keras.layers.Layer):\n",
        "    def __init__(self, units=32):\n",
        "        super(Linear, self).__init__()\n",
        "        self.units = units\n",
        "\n",
        "    def build(self, input_shape):\n",
        "        self.w = self.add_weight(\n",
        "            shape=(input_shape[-1], self.units),\n",
        "            initializer=\"random_normal\",\n",
        "            trainable=True,\n",
        "        )\n",
        "        self.b = self.add_weight(\n",
        "            shape=(self.units,), initializer=\"random_normal\", trainable=True\n",
        "        )\n",
        "\n",
        "    def call(self, inputs):\n",
        "        return tf.matmul(inputs, self.w) + self.b\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "78061e0583c6"
      },
      "source": [
        "레이어의 `__call__()` 메서드는 처음 호출될 때 자동으로 빌드를 실행합니다. 지연되어 사용하기 쉬운 레이어입니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "fb08b1a45d22"
      },
      "outputs": [],
      "source": [
        "# At instantiation, we don't know on what inputs this is going to get called\n",
        "linear_layer = Linear(32)\n",
        "\n",
        "# The layer's weights are created dynamically the first time the layer is called\n",
        "y = linear_layer(x)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "ddd7e8b22641"
      },
      "source": [
        "## 재귀적으로 구성 가능한 레이어\n",
        "\n",
        "또 다른 인스턴스의 속성으로 Layer 인스턴스를 할당하면 외부 레이어가 내부 레이어의 가중치를 추적하기 시작합니다.\n",
        "\n",
        "`__init__()` 메서드에서 이러한 하위 레이어를 만드는 것이 좋습니다(하위 레이어에는 일반적으로 빌드 메서드가 있으므로 외부 레이어가 만들어질 때 빌드됩니다)."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "9561cbf2fc60"
      },
      "outputs": [],
      "source": [
        "# Let's assume we are reusing the Linear class\n",
        "# with a `build` method that we defined above.\n",
        "\n",
        "\n",
        "class MLPBlock(keras.layers.Layer):\n",
        "    def __init__(self):\n",
        "        super(MLPBlock, self).__init__()\n",
        "        self.linear_1 = Linear(32)\n",
        "        self.linear_2 = Linear(32)\n",
        "        self.linear_3 = Linear(1)\n",
        "\n",
        "    def call(self, inputs):\n",
        "        x = self.linear_1(inputs)\n",
        "        x = tf.nn.relu(x)\n",
        "        x = self.linear_2(x)\n",
        "        x = tf.nn.relu(x)\n",
        "        return self.linear_3(x)\n",
        "\n",
        "\n",
        "mlp = MLPBlock()\n",
        "y = mlp(tf.ones(shape=(3, 64)))  # The first call to the `mlp` will create the weights\n",
        "print(\"weights:\", len(mlp.weights))\n",
        "print(\"trainable weights:\", len(mlp.trainable_weights))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "496736d98a62"
      },
      "source": [
        "## `add_loss()` 메서드\n",
        "\n",
        "레이어의 `call()` 메서드를 작성할 때는 훈련 루프를 작성할 때 나중에 사용하려는 손실 텐서를 만들 수 있습니다. `self.add_loss(value)`를 호출하면 됩니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "084d56602ca4"
      },
      "outputs": [],
      "source": [
        "# A layer that creates an activity regularization loss\n",
        "class ActivityRegularizationLayer(keras.layers.Layer):\n",
        "    def __init__(self, rate=1e-2):\n",
        "        super(ActivityRegularizationLayer, self).__init__()\n",
        "        self.rate = rate\n",
        "\n",
        "    def call(self, inputs):\n",
        "        self.add_loss(self.rate * tf.reduce_sum(inputs))\n",
        "        return inputs\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "5009ff0d1feb"
      },
      "source": [
        "이러한 손실(내부 레이어에서 생성된 손실 포함)은 `layer.losses`를 통해 검색할 수 있습니다. 이 속성은 모든 `__call__()`이 시작될 때 최상위 레이어로 재설정되므로 `layer.losses`에는 항상 마지막 정방향 패스에서 생성된 손실값이 포함됩니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "5bff6d59aea7"
      },
      "outputs": [],
      "source": [
        "class OuterLayer(keras.layers.Layer):\n",
        "    def __init__(self):\n",
        "        super(OuterLayer, self).__init__()\n",
        "        self.activity_reg = ActivityRegularizationLayer(1e-2)\n",
        "\n",
        "    def call(self, inputs):\n",
        "        return self.activity_reg(inputs)\n",
        "\n",
        "\n",
        "layer = OuterLayer()\n",
        "assert len(layer.losses) == 0  # No losses yet since the layer has never been called\n",
        "\n",
        "_ = layer(tf.zeros(1, 1))\n",
        "assert len(layer.losses) == 1  # We created one loss value\n",
        "\n",
        "# `layer.losses` gets reset at the start of each __call__\n",
        "_ = layer(tf.zeros(1, 1))\n",
        "assert len(layer.losses) == 1  # This is the loss created during the call above"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "36751ebe3363"
      },
      "source": [
        "또한, `loss` 속성에는 내부 레이어의 가중치에 대해 생성된 정규화 손실도 포함됩니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "9327d3b581f8"
      },
      "outputs": [],
      "source": [
        "class OuterLayerWithKernelRegularizer(keras.layers.Layer):\n",
        "    def __init__(self):\n",
        "        super(OuterLayerWithKernelRegularizer, self).__init__()\n",
        "        self.dense = keras.layers.Dense(\n",
        "            32, kernel_regularizer=tf.keras.regularizers.l2(1e-3)\n",
        "        )\n",
        "\n",
        "    def call(self, inputs):\n",
        "        return self.dense(inputs)\n",
        "\n",
        "\n",
        "layer = OuterLayerWithKernelRegularizer()\n",
        "_ = layer(tf.zeros((1, 1)))\n",
        "\n",
        "# This is `1e-3 * sum(layer.dense.kernel ** 2)`,\n",
        "# created by the `kernel_regularizer` above.\n",
        "print(layer.losses)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "99d502b8899c"
      },
      "source": [
        "이러한 손실은 다음과 같이 훈련 루프를 작성할 때 고려됩니다.\n",
        "\n",
        "```python\n",
        "# Instantiate an optimizer.\n",
        "optimizer = tf.keras.optimizers.SGD(learning_rate=1e-3)\n",
        "loss_fn = keras.losses.SparseCategoricalCrossentropy(from_logits=True)\n",
        "\n",
        "# Iterate over the batches of a dataset.\n",
        "for x_batch_train, y_batch_train in train_dataset:\n",
        "  with tf.GradientTape() as tape:\n",
        "    logits = layer(x_batch_train)  # Logits for this minibatch\n",
        "    # Loss value for this minibatch\n",
        "    loss_value = loss_fn(y_batch_train, logits)\n",
        "    # Add extra losses created during this forward pass:\n",
        "    loss_value += sum(model.losses)\n",
        "\n",
        "  grads = tape.gradient(loss_value, model.trainable_weights)\n",
        "  optimizer.apply_gradients(zip(grads, model.trainable_weights))\n",
        "```"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "7fa2db4a631d"
      },
      "source": [
        "훈련 루프 작성에 대한 자세한 가이드는 [처음부터 훈련 루프 작성하기 가이드](https://www.tensorflow.org/guide/keras/writing_a_training_loop_from_scratch/)를 참조하세요.\n",
        "\n",
        "이러한 손실은 `fit()`에서도 완벽하게 작동합니다(손실이 있는 경우, 자동으로 합산되어 주 손실에 추가됨)."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "7c534372bf8a"
      },
      "outputs": [],
      "source": [
        "import numpy as np\n",
        "\n",
        "inputs = keras.Input(shape=(3,))\n",
        "outputs = ActivityRegularizationLayer()(inputs)\n",
        "model = keras.Model(inputs, outputs)\n",
        "\n",
        "# If there is a loss passed in `compile`, thee regularization\n",
        "# losses get added to it\n",
        "model.compile(optimizer=\"adam\", loss=\"mse\")\n",
        "model.fit(np.random.random((2, 3)), np.random.random((2, 3)))\n",
        "\n",
        "# It's also possible not to pass any loss in `compile`,\n",
        "# since the model already has a loss to minimize, via the `add_loss`\n",
        "# call during the forward pass!\n",
        "model.compile(optimizer=\"adam\")\n",
        "model.fit(np.random.random((2, 3)), np.random.random((2, 3)))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "c753fcbc1818"
      },
      "source": [
        "## `add_metric()` 메서드\n",
        "\n",
        "`add_loss()`와 마찬가지로, 레이어에는 훈련 중 수량의 이동 평균을 추적하기 위한 `add_metric()` 메서드도 있습니다.\n",
        "\n",
        "다음 \"로지스틱 엔드포인트\" 레이어를 고려합니다. 입력 예측 및 목표치로 사용하여 `add_loss()`를 통해 추적하는 손실을 계산하고 `add_metric()`을 통해 추적하는 정확도 스칼라를 계산합니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "85dad61dc160"
      },
      "outputs": [],
      "source": [
        "class LogisticEndpoint(keras.layers.Layer):\n",
        "    def __init__(self, name=None):\n",
        "        super(LogisticEndpoint, self).__init__(name=name)\n",
        "        self.loss_fn = keras.losses.BinaryCrossentropy(from_logits=True)\n",
        "        self.accuracy_fn = keras.metrics.BinaryAccuracy()\n",
        "\n",
        "    def call(self, targets, logits, sample_weights=None):\n",
        "        # Compute the training-time loss value and add it\n",
        "        # to the layer using `self.add_loss()`.\n",
        "        loss = self.loss_fn(targets, logits, sample_weights)\n",
        "        self.add_loss(loss)\n",
        "\n",
        "        # Log accuracy as a metric and add it\n",
        "        # to the layer using `self.add_metric()`.\n",
        "        acc = self.accuracy_fn(targets, logits, sample_weights)\n",
        "        self.add_metric(acc, name=\"accuracy\")\n",
        "\n",
        "        # Return the inference-time prediction tensor (for `.predict()`).\n",
        "        return tf.nn.softmax(logits)\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "cd8807cb9cbc"
      },
      "source": [
        "이러한 방식으로 추적되는 메트릭은 `layer.metrics`를 통해 액세스할 수 있습니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "475df7270265"
      },
      "outputs": [],
      "source": [
        "layer = LogisticEndpoint()\n",
        "\n",
        "targets = tf.ones((2, 2))\n",
        "logits = tf.ones((2, 2))\n",
        "y = layer(targets, logits)\n",
        "\n",
        "print(\"layer.metrics:\", layer.metrics)\n",
        "print(\"current accuracy value:\", float(layer.metrics[0].result()))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "9eda5113fd18"
      },
      "source": [
        "`add_loss()`와 마찬가지로, 이러한 메트릭은 `fit()`의해 추적됩니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "97f767613953"
      },
      "outputs": [],
      "source": [
        "inputs = keras.Input(shape=(3,), name=\"inputs\")\n",
        "targets = keras.Input(shape=(10,), name=\"targets\")\n",
        "logits = keras.layers.Dense(10)(inputs)\n",
        "predictions = LogisticEndpoint(name=\"predictions\")(logits, targets)\n",
        "\n",
        "model = keras.Model(inputs=[inputs, targets], outputs=predictions)\n",
        "model.compile(optimizer=\"adam\")\n",
        "\n",
        "data = {\n",
        "    \"inputs\": np.random.random((3, 3)),\n",
        "    \"targets\": np.random.random((3, 10)),\n",
        "}\n",
        "model.fit(data)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "4bdbac3f6c85"
      },
      "source": [
        "## 레이어에서 선택적으로 직렬화를 활성화할 수 있습니다\n",
        "\n",
        "[함수 모델](https://www.tensorflow.org/guide/keras/functional/)의 일부로 사용자 정의 레이어를 직렬화해야 하는 경우, 선택적으로 `get_config()` 메서드를 구현할 수 있습니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "b359ed5289a8"
      },
      "outputs": [],
      "source": [
        "class Linear(keras.layers.Layer):\n",
        "    def __init__(self, units=32):\n",
        "        super(Linear, self).__init__()\n",
        "        self.units = units\n",
        "\n",
        "    def build(self, input_shape):\n",
        "        self.w = self.add_weight(\n",
        "            shape=(input_shape[-1], self.units),\n",
        "            initializer=\"random_normal\",\n",
        "            trainable=True,\n",
        "        )\n",
        "        self.b = self.add_weight(\n",
        "            shape=(self.units,), initializer=\"random_normal\", trainable=True\n",
        "        )\n",
        "\n",
        "    def call(self, inputs):\n",
        "        return tf.matmul(inputs, self.w) + self.b\n",
        "\n",
        "    def get_config(self):\n",
        "        return {\"units\": self.units}\n",
        "\n",
        "\n",
        "# Now you can recreate the layer from its config:\n",
        "layer = Linear(64)\n",
        "config = layer.get_config()\n",
        "print(config)\n",
        "new_layer = Linear.from_config(config)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "78b207f7acbc"
      },
      "source": [
        "기본 `Layer` 클래스의 `__init__()` 메서드는 일부 키워드 인수, 특히 `name` 및 `dtype`를 사용합니다. 이러한 인수를 `__init__()`의 부모 클래스에 전달하고 레이어 구성에 포함하는 것이 좋습니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "00a3432cd28c"
      },
      "outputs": [],
      "source": [
        "class Linear(keras.layers.Layer):\n",
        "    def __init__(self, units=32, **kwargs):\n",
        "        super(Linear, self).__init__(**kwargs)\n",
        "        self.units = units\n",
        "\n",
        "    def build(self, input_shape):\n",
        "        self.w = self.add_weight(\n",
        "            shape=(input_shape[-1], self.units),\n",
        "            initializer=\"random_normal\",\n",
        "            trainable=True,\n",
        "        )\n",
        "        self.b = self.add_weight(\n",
        "            shape=(self.units,), initializer=\"random_normal\", trainable=True\n",
        "        )\n",
        "\n",
        "    def call(self, inputs):\n",
        "        return tf.matmul(inputs, self.w) + self.b\n",
        "\n",
        "    def get_config(self):\n",
        "        config = super(Linear, self).get_config()\n",
        "        config.update({\"units\": self.units})\n",
        "        return config\n",
        "\n",
        "\n",
        "layer = Linear(64)\n",
        "config = layer.get_config()\n",
        "print(config)\n",
        "new_layer = Linear.from_config(config)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "ad5d100cc969"
      },
      "source": [
        "구성에서 레이어를 역직렬화할 때 유연성이 더 필요한 경우, `from_config()` 클래스 메서드를 재정의할 수도 있습니다. 다음은 `from_config()`의 기본 구현입니다.\n",
        "\n",
        "```python\n",
        "def from_config(cls, config):\n",
        "  return cls(**config)\n",
        "```\n",
        "\n",
        "직렬화 및 저장에 대한 자세한 내용은 [모델 저장 및 직렬화 가이드](https://www.tensorflow.org/guide/keras/save_and_serialize/)를 참조하세요."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "741c6d134d65"
      },
      "source": [
        "## `call()` 메서드의 권한 있는 `training` 인수\n",
        "\n",
        "일부 레이어, 특히 `BatchNormalization` 레이어와 `Dropout` 레이어는 훈련 및 추론 중에 서로 다른 동작을 갖습니다. 이러한 레이어의 경우, `call()` 메서드에서 `training`(boolean) 인수를 노출하는 것이 표준 관행입니다.\n",
        "\n",
        "이 인수를 `call()`에서 노출하면 내장 훈련 및 평가 루프(예: `fit()`)를 사용하여 훈련 및 추론에서 레이어를 올바르게 사용할 수 있습니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "67ca741d0cfb"
      },
      "outputs": [],
      "source": [
        "class CustomDropout(keras.layers.Layer):\n",
        "    def __init__(self, rate, **kwargs):\n",
        "        super(CustomDropout, self).__init__(**kwargs)\n",
        "        self.rate = rate\n",
        "\n",
        "    def call(self, inputs, training=None):\n",
        "        if training:\n",
        "            return tf.nn.dropout(inputs, rate=self.rate)\n",
        "        return inputs\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "5284e22677da"
      },
      "source": [
        "## `call()` 메서드의 권한 있는 `mask` 인수\n",
        "\n",
        "`call()`에서 지원되는 다른 권한 있는 인수는 `mask` 인수입니다.\n",
        "\n",
        "이 인수는 모든 Keras RNN 레이어에서 볼 수 있습니다. 마스크는 시계열 데이터를 처리할 때 특정 입력 타임스텝을 건너뛰는 데 사용되는 부울 텐서(입력의 타임스텝당 하나의 부울 값)입니다.\n",
        "\n",
        "Keras는 이전 레이어에서 마스크가 생성될 때 이를 지원하는 레이어에 대해 올바른 `mask` 인수를 `__call__()`에 자동으로 전달합니다. 마스크 생성 레이어는 `mask_zero=True` 레이어와 `Masking` 레이어로 구성된  `Embedding`입니다.\n",
        "\n",
        "마스킹 및 마스킹 지원 레이어를 작성하는 방법에 대한 자세한 내용은 [\"패딩 및 마스킹 이해하기\"](https://www.tensorflow.org/guide/keras/masking_and_padding/) 가이드를 확인하세요."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "bf87358118de"
      },
      "source": [
        "## `Model` 클래스\n",
        "\n",
        "일반적으로, `Layer` 클래스를 사용하여 내부 계산 블록을 정의하고 `Model` 클래스를 사용하여 훈련할 객체인 외부 모델을 정의합니다.\n",
        "\n",
        "예를 들어, ResNet50 모델에는 `Layer`를 하위 클래스화하는 여러 ResNet 블록과 전체 ResNet50 네트워크를 포괄하는 단일 `Model`이 있습니다.\n",
        "\n",
        "`Model` 클래스는 `Layer`와 같은 API를 가지며, 다음과 같은 차이점이 있습니다.\n",
        "\n",
        "- 내장 훈련, 평가 및 예측 루프( `model.fit()` , `model.evaluate()`, `model.predict()`)를 제공합니다.\n",
        "- `model.layers` 속성을 통해 내부 레이어의 목록을 노출합니다.\n",
        "- 저장 및 직렬화 API(`save()`, `save_weights()`...)를 노출합니다.\n",
        "\n",
        "효과적으로, `Layer` 클래스는 문서에서 일컫는 \"레이어\"(\"컨볼루션 레이어\" 또는 \"되풀이 레이어\"에서와 같이) 또는 \"블록\"(\"ResNet 블록\" 또는 \"Inception 블록\"에서와 같이)에 해당합니다.\n",
        "\n",
        "한편, `Model` 클래스는 문서에서 \"모델\"(\"딥 러닝 모델\"에서) 또는 \"네트워크\"( \"딥 신경망\"에서)로 지칭되는 것에 해당합다.\n",
        "\n",
        "\"`Layer` 클래스를 사용해야 할까요? 아니면 `Model` 클래스를 사용해야 할까요?\"라는 질문이 있다면 자문해 보세요. `fit()`을 호출해야 할까? `save()`를 호출해야 할까? 만약 그렇다면 `Model`를 사용하세요. 그렇지 않다면(클래스가 더 큰 시스템의 블록이거나 직접 훈련을 작성하고 코드를 저장하기 때문에) `Layer`를 사용하세요.\n",
        "\n",
        "예를 들어, 위의 mini-resnet 예제를 사용하여 `fit()`으로 훈련하고 `save_weights()`로 저장할 수 있는 `Model`을 빌드할 수 있습니다."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "78539e55de0c"
      },
      "source": [
        "```python\n",
        "class ResNet(tf.keras.Model):\n",
        "\n",
        "    def __init__(self):\n",
        "        super(ResNet, self).__init__()\n",
        "        self.block_1 = ResNetBlock()\n",
        "        self.block_2 = ResNetBlock()\n",
        "        self.global_pool = layers.GlobalAveragePooling2D()\n",
        "        self.classifier = Dense(num_classes)\n",
        "\n",
        "    def call(self, inputs):\n",
        "        x = self.block_1(inputs)\n",
        "        x = self.block_2(x)\n",
        "        x = self.global_pool(x)\n",
        "        return self.classifier(x)\n",
        "\n",
        "\n",
        "resnet = ResNet()\n",
        "dataset = ...\n",
        "resnet.fit(dataset, epochs=10)\n",
        "resnet.save(filepath)\n",
        "```"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "b817a4de8c5d"
      },
      "source": [
        "## 종합: 엔드 투 엔드 예제\n",
        "\n",
        "지금까지 배운 내용은 다음과 같습니다.\n",
        "\n",
        "- `Layer`는 상태(`__init__()` 또는 `build()`) 및 일부 계산(`call()`에서 정의)을 캡슐화합니다.\n",
        "- 레이어를 재귀적으로 중첩하여 새롭고 더 큰 계산 블록을 만들 수 있습니다.\n",
        "- 레이어는 `add_loss()` 및 `add_metric()`을 통해 메트릭뿐만 아니라 손실(일반적으로, 정규화 손실)을 생성 및 추적할 수 있습니다.\n",
        "- 훈련하려는 외부 컨테이너는 `Model`입니다. `Model`은 `Layer`와 비슷하지만, 훈련 및 직렬화 유틸리티가 추가되었습니다.\n",
        "\n",
        "이 모든 것을 엔드 투 엔드 예제에 넣어봅시다. VAE(Variational AutoEncoder)를 구현할 것이며, MNIST 숫자로 훈련할 것입니다.\n",
        "\n",
        "VAE는 `Model`의 서브 클래스가 될 것이며 `Layer`를 하위 클래스화하는 중첩된 레이어 구성으로 빌드됩니다. 정규화 손실(KL 확산)을 제공합니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "18842173f875"
      },
      "outputs": [],
      "source": [
        "from tensorflow.keras import layers\n",
        "\n",
        "\n",
        "class Sampling(layers.Layer):\n",
        "    \"\"\"Uses (z_mean, z_log_var) to sample z, the vector encoding a digit.\"\"\"\n",
        "\n",
        "    def call(self, inputs):\n",
        "        z_mean, z_log_var = inputs\n",
        "        batch = tf.shape(z_mean)[0]\n",
        "        dim = tf.shape(z_mean)[1]\n",
        "        epsilon = tf.keras.backend.random_normal(shape=(batch, dim))\n",
        "        return z_mean + tf.exp(0.5 * z_log_var) * epsilon\n",
        "\n",
        "\n",
        "class Encoder(layers.Layer):\n",
        "    \"\"\"Maps MNIST digits to a triplet (z_mean, z_log_var, z).\"\"\"\n",
        "\n",
        "    def __init__(self, latent_dim=32, intermediate_dim=64, name=\"encoder\", **kwargs):\n",
        "        super(Encoder, self).__init__(name=name, **kwargs)\n",
        "        self.dense_proj = layers.Dense(intermediate_dim, activation=\"relu\")\n",
        "        self.dense_mean = layers.Dense(latent_dim)\n",
        "        self.dense_log_var = layers.Dense(latent_dim)\n",
        "        self.sampling = Sampling()\n",
        "\n",
        "    def call(self, inputs):\n",
        "        x = self.dense_proj(inputs)\n",
        "        z_mean = self.dense_mean(x)\n",
        "        z_log_var = self.dense_log_var(x)\n",
        "        z = self.sampling((z_mean, z_log_var))\n",
        "        return z_mean, z_log_var, z\n",
        "\n",
        "\n",
        "class Decoder(layers.Layer):\n",
        "    \"\"\"Converts z, the encoded digit vector, back into a readable digit.\"\"\"\n",
        "\n",
        "    def __init__(self, original_dim, intermediate_dim=64, name=\"decoder\", **kwargs):\n",
        "        super(Decoder, self).__init__(name=name, **kwargs)\n",
        "        self.dense_proj = layers.Dense(intermediate_dim, activation=\"relu\")\n",
        "        self.dense_output = layers.Dense(original_dim, activation=\"sigmoid\")\n",
        "\n",
        "    def call(self, inputs):\n",
        "        x = self.dense_proj(inputs)\n",
        "        return self.dense_output(x)\n",
        "\n",
        "\n",
        "class VariationalAutoEncoder(keras.Model):\n",
        "    \"\"\"Combines the encoder and decoder into an end-to-end model for training.\"\"\"\n",
        "\n",
        "    def __init__(\n",
        "        self,\n",
        "        original_dim,\n",
        "        intermediate_dim=64,\n",
        "        latent_dim=32,\n",
        "        name=\"autoencoder\",\n",
        "        **kwargs\n",
        "    ):\n",
        "        super(VariationalAutoEncoder, self).__init__(name=name, **kwargs)\n",
        "        self.original_dim = original_dim\n",
        "        self.encoder = Encoder(latent_dim=latent_dim, intermediate_dim=intermediate_dim)\n",
        "        self.decoder = Decoder(original_dim, intermediate_dim=intermediate_dim)\n",
        "\n",
        "    def call(self, inputs):\n",
        "        z_mean, z_log_var, z = self.encoder(inputs)\n",
        "        reconstructed = self.decoder(z)\n",
        "        # Add KL divergence regularization loss.\n",
        "        kl_loss = -0.5 * tf.reduce_mean(\n",
        "            z_log_var - tf.square(z_mean) - tf.exp(z_log_var) + 1\n",
        "        )\n",
        "        self.add_loss(kl_loss)\n",
        "        return reconstructed\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "40384d934b3c"
      },
      "source": [
        "MNIST에 간단한 훈련 루프를 작성해 봅시다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "c37fef01d4bc"
      },
      "outputs": [],
      "source": [
        "original_dim = 784\n",
        "vae = VariationalAutoEncoder(original_dim, 64, 32)\n",
        "\n",
        "optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3)\n",
        "mse_loss_fn = tf.keras.losses.MeanSquaredError()\n",
        "\n",
        "loss_metric = tf.keras.metrics.Mean()\n",
        "\n",
        "(x_train, _), _ = tf.keras.datasets.mnist.load_data()\n",
        "x_train = x_train.reshape(60000, 784).astype(\"float32\") / 255\n",
        "\n",
        "train_dataset = tf.data.Dataset.from_tensor_slices(x_train)\n",
        "train_dataset = train_dataset.shuffle(buffer_size=1024).batch(64)\n",
        "\n",
        "epochs = 2\n",
        "\n",
        "# Iterate over epochs.\n",
        "for epoch in range(epochs):\n",
        "    print(\"Start of epoch %d\" % (epoch,))\n",
        "\n",
        "    # Iterate over the batches of the dataset.\n",
        "    for step, x_batch_train in enumerate(train_dataset):\n",
        "        with tf.GradientTape() as tape:\n",
        "            reconstructed = vae(x_batch_train)\n",
        "            # Compute reconstruction loss\n",
        "            loss = mse_loss_fn(x_batch_train, reconstructed)\n",
        "            loss += sum(vae.losses)  # Add KLD regularization loss\n",
        "\n",
        "        grads = tape.gradient(loss, vae.trainable_weights)\n",
        "        optimizer.apply_gradients(zip(grads, vae.trainable_weights))\n",
        "\n",
        "        loss_metric(loss)\n",
        "\n",
        "        if step % 100 == 0:\n",
        "            print(\"step %d: mean loss = %.4f\" % (step, loss_metric.result()))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "65e5faeb0029"
      },
      "source": [
        "VAE는 `Model`을 하위 클래스화하기 때문에 내장된 훈련 루프를 제공합니다. 따라서 다음과 같이 훈련할 수도 있습니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "1e98ba7ebdb8"
      },
      "outputs": [],
      "source": [
        "vae = VariationalAutoEncoder(784, 64, 32)\n",
        "\n",
        "optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3)\n",
        "\n",
        "vae.compile(optimizer, loss=tf.keras.losses.MeanSquaredError())\n",
        "vae.fit(x_train, x_train, epochs=2, batch_size=64)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "42ee3169e70c"
      },
      "source": [
        "## 객체 지향 개발을 넘어: 함수형 API\n",
        "\n",
        "이 예제가 너무 지나친 객체 지향 개발입니까? [함수형 API(Functional API)](https://www.tensorflow.org/guide/keras/functional/)를 사용하여 모델을 빌드할 수도 있습니다. 중요한 것은 하나의 스타일을 선택한다고 해서 다른 스타일로 작성된 구성 요소를 활용하지 못하는 것은 아닙니다. 항상 목적에 따라 다르게 선택할 수 있습니다.\n",
        "\n",
        "예를 들어, 아래의 함수형 API 예제는 위 예제에서 정의한 것과 같은 `Sampling` 레이어를 재사용합니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "b8fe39f892c7"
      },
      "outputs": [],
      "source": [
        "original_dim = 784\n",
        "intermediate_dim = 64\n",
        "latent_dim = 32\n",
        "\n",
        "# Define encoder model.\n",
        "original_inputs = tf.keras.Input(shape=(original_dim,), name=\"encoder_input\")\n",
        "x = layers.Dense(intermediate_dim, activation=\"relu\")(original_inputs)\n",
        "z_mean = layers.Dense(latent_dim, name=\"z_mean\")(x)\n",
        "z_log_var = layers.Dense(latent_dim, name=\"z_log_var\")(x)\n",
        "z = Sampling()((z_mean, z_log_var))\n",
        "encoder = tf.keras.Model(inputs=original_inputs, outputs=z, name=\"encoder\")\n",
        "\n",
        "# Define decoder model.\n",
        "latent_inputs = tf.keras.Input(shape=(latent_dim,), name=\"z_sampling\")\n",
        "x = layers.Dense(intermediate_dim, activation=\"relu\")(latent_inputs)\n",
        "outputs = layers.Dense(original_dim, activation=\"sigmoid\")(x)\n",
        "decoder = tf.keras.Model(inputs=latent_inputs, outputs=outputs, name=\"decoder\")\n",
        "\n",
        "# Define VAE model.\n",
        "outputs = decoder(z)\n",
        "vae = tf.keras.Model(inputs=original_inputs, outputs=outputs, name=\"vae\")\n",
        "\n",
        "# Add KL divergence regularization loss.\n",
        "kl_loss = -0.5 * tf.reduce_mean(z_log_var - tf.square(z_mean) - tf.exp(z_log_var) + 1)\n",
        "vae.add_loss(kl_loss)\n",
        "\n",
        "# Train.\n",
        "optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3)\n",
        "vae.compile(optimizer, loss=tf.keras.losses.MeanSquaredError())\n",
        "vae.fit(x_train, x_train, epochs=3, batch_size=64)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "0f4db7df7eb5"
      },
      "source": [
        "자세한 정보는 [함수형 API 가이드](https://www.tensorflow.org/guide/keras/functional/)를 참고하세요."
      ]
    }
  ],
  "metadata": {
    "colab": {
      "collapsed_sections": [],
      "name": "custom_layers_and_models.ipynb",
      "toc_visible": true
    },
    "kernelspec": {
      "display_name": "Python 3",
      "name": "python3"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}
